package hyl.base.zk;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * zkreserve 主备方式 <br>
 * 不同于集群方式zkspare,zkreserve方案下每次只有一个程序在工作
 * 
 * 这个类的用途是 如果当前节点异常,就切换到备用节点<br>
 * 
 * 之前我们都是单机节点的多线程 来解决异常线程的 恢复<br>
 * 在分布式环境 如果某台电脑故障了，我们可以使用备用节点的程序<br>
 * 这就是这个组件设计的初衷<br>
 * 
 * 借鉴了其他设计方案<br>
 * 
 * （方法2) 监听前一个节点变化 本类就是用了这种方法<br>
 * 缺点是如果多个节点同时掉线 ,可能导致程序错误<br>
 * 一开始顺序启动多个节点<br>
 * 如果第一个节点故障会自动切换到第二节点<br>
 * 如果中间节点故障 就安排后续节点监听前一个节点<br>
 * 
 * （方法1）监听所有节点变化 <br>
 *  1/每次注册，就修改父节点 data，并监听父节点数据变化<br>
 *  2/有节点离线也修改父节点data=临时节点排序组合，<br>
 *  3/所有节点重新监听父节点变化,如果自身在第一位就启动程序<br>
 *  4/利用zk 实现部署在不同主机上的程序 ,如果异常自动启动<br>
 * 
 * @author 37798955@qq.com
 *
 */
public class ZkReserve {
	private static final Logger logger = LoggerFactory.getLogger(ZkReserve.class);
	MyZK _zk = null;
	/** projectDir 指向 工程==zk服务器上的项目节点  */
	String projectDir = null;

	/** processID 指向 工程下的当前进程id = 在zk服务器上的临时节点名称 */
	String processID = null;
	
	/** processPath 指向 工程下的当前进程 =项目节点+进程id */
	String processPath = null;
	/**
	 * 进程别名 保存在节点的data中
	 */
//	String processName = "";

	/** 进程主函数,从外部通过runnable接口注入 */
	Runnable _fun = null;

	DisconnWatcher zkWatcher = new DisconnWatcher();

	/**
	 * 
	 * @param 虚拟项目路径 末尾不能带'/'
	 * @param 节点名
	 * @param zk服务器
	 * @param 心跳间隔   ms
	 * @param 回调函数   如果当前程序开始接管工作,就调用回调函数
	 * @throws InterruptedException 
	 * @throws IOException 
	 */

	public ZkReserve(String 虚拟项目路径,MyZK zk服务器 , Runnable 回调函数) throws IOException, InterruptedException {
		projectDir = 虚拟项目路径;
	
		_fun = 回调函数;
		_zk= zk服务器;//new MyZK(zk服务器, 监听间隔)
	}

	public String getProcessPath() {
		return processPath;
	}

	void setProcessPath(String processPath) {
		this.processID = processPath.substring(projectDir.length() + 1);
		this.processPath = projectDir + "/" + processID;
	}

	public List<String> getChildren() {
		return _zk.getChildren(projectDir);
	}

	public void start() {
		// ("192.168.0.113:2181",5000);
		// 创建获取父节点
		_zk.createPNodes(projectDir);// 如果父节点不存在 补上
		if (null == processID) {
			String path = _zk.createTNode(projectDir, null); // 创建项目的临时节点==注册一个新进程
			// System.out.println("创建"+path);
			setProcessPath(path);// 存放路径
			// System.out.println("路径="+processPath);
			// 锁住节点并等待 启用通知或切换通知
			// while (true) {
			int st = watch(); // 监听前一个节点
			//System.out.println("状态=" + st);
			if (st == 1) {
				// 解锁成功,获取操作句柄, 调用主函数
				;
				// break;
			} else if (st == 0) {
				zkWatcher.await();// 进入等待状态以后 ,只有通过DisconnWatcher解锁才能运行后续程序
			} else {
				logger.error("zookeeper 服务器异常，无法注册节点" + processPath + "，请检查zk日志");
				return;
			}
			// }
			if (_fun != null)
				_fun.run();
		}
	}
	//监控首个节点
	int watch() {
		List<String> nodeList = _zk.getChildren(projectDir);
		if (nodeList==null||nodeList.size()==0)
			return -1;		
		Collections.sort(nodeList);	
		String first=nodeList.get(0);
		if (first.equals(processID)) return 1;//唤醒
		else {_zk.exists(projectDir + "/" +first, zkWatcher);//监听主节点,继续沉睡
		return 0;}		
	}
	//监控前一个节点
	int watch2() {
		List<String> nodeList = _zk.getChildren(projectDir);
		Collections.sort(nodeList);
		// System.out.println(projectDir);
		//for (String s : nodeList) {
		//	System.out.println(s);
		//}
		// System.out.println("--");
		System.out.println(processID);
		// 查找子节点索引
		int index = nodeList.indexOf(processID);
		switch (index) {
		case -1:
			// System.out.println("本节点已不在了" + lockPath);
			return -1; // 异常
		case 0: // 如果在第一个位置
			return 1; // 可以复活
		default: // 如果不需要复活,继续监听上一个人
			String waitNode = nodeList.get(--index);
			_zk.exists(projectDir + "/" + waitNode, zkWatcher);// 把监听器移到另一个人身上
			return 0;// 请继续等待
		}
	}

	/**
	 * 
	 * 离线状态监听器<br>
	 * 
	 * 如果缺少父节点创建父节点 然后在父节点下面创建临时节点 创建临时节点
	 * 
	 * @author 37798955@qq.com
	 *
	 */
	class DisconnWatcher implements Watcher {
		private CountDownLatch latch = new CountDownLatch(1);

		/*
		 * watchedEvent 被监听的节点事件 如果当前 监听到 某个节点 离线 那么 1 判断是否需要复活,如果是第一个节点就复活 2
		 * 如果不需要复活,重新监听前一个人
		 */
		@Override
		public void process(WatchedEvent event) {
			if (event.getType() == EventType.NodeDeleted) {
				if (watch() == 1)// 如果唤醒
				{
					logger.error("主工作程序异常停止,准备激活当前程序");
					latch.countDown(); // 激活指令
				}
			}
		}

		// 等待激活
		public void await() {
			try {
				latch.await();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

	}
}
